import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import nl.jaapniek.db.DBManager;
import nl.jaapniek.exception.RdbmsException;
import nl.jaapniek.model.Criteria;
import nl.jaapniek.model.Database;
import nl.jaapniek.model.ForeignKey;
import nl.jaapniek.model.Groep;
import nl.jaapniek.model.GroepAttr;
import nl.jaapniek.model.Tabel;
import nl.jaapniek.model.Veld;

public class Rdbms {

	private List<Database> databases;
	private List<Database> nieuweDatabases;
	private List<Database> gedropteDatabases;
	private Database database;

	public Rdbms() {
		databases = new ArrayList<Database>();
		nieuweDatabases = new ArrayList<Database>();
		gedropteDatabases = new ArrayList<Database>();
	}

	// ============================= DDL =================================

	public void createDatabase(String naam) throws RdbmsException {
		for(Database db : databases){
			if(db.getNaam().equals(naam)){
				throw new RdbmsException("Er bestaat al een database met deze naam");
			}
		}
		Database db = new Database();
		db.setNaam(naam);
		databases.add(db);
		nieuweDatabases.add(db);
	}

	public void dropDatabase(String naam) throws RdbmsException {
		Database removed = removeDatabase(naam);
		if (removed != null) {
			gedropteDatabases.add(removed);
		}else{
			throw new RdbmsException("Geen database gevonden met deze naam");
		}
	}

	public Database useDatabase(String naam) throws RdbmsException {
		database = getDatabase(naam);
		if(database == null)
			throw new RdbmsException("Geen database gevonden met deze naam");
		return database;
	}

	public Tabel createTable(String tabelnaam, List<Veld> velden,
			List<Groep> groepen, List<ForeignKey> fkeys) throws RdbmsException {
		if(velden.size() < 1){
			throw new RdbmsException("Tabel moet velden bevatten!");
		}
		Tabel check = getTabel(tabelnaam);
		if(check != null){
			throw new RdbmsException("Tabel met deze naam bestaat al!");
		}
		Tabel tabel = new Tabel(tabelnaam);
		for (Veld veld : velden) {
			tabel.addVeld(veld);
		}
		boolean hasPrimary = false;
		for (Groep groep : groepen) {
			if (groep.isPrimary()) {
				hasPrimary = true;
			}
			for (GroepAttr attr : groep.getGroepAttrs()) {
				if (!velden.contains(attr.getVeld())) {
					throw new RdbmsException(
							"Velden in een groep moeten in dezelfde tabel bestaan!");
				}
			}
		}
		if (hasPrimary) {
			for (Groep groep : groepen) {
				if (groep.isPrimary()
						|| (!groep.isPrimary() && !groep.isAlternative())) {
					groep.setTabel(tabel);
					groep.setDatabase(database);
					for (GroepAttr attr : groep.getGroepAttrs()) {
						attr.setTabel(tabel);
					}
					database.addGroep(groep);
				} else {
					throw new RdbmsException(
							"createTable groepen mogen alleen primary keys of foreign keys zijn!");
				}
			}
			if (fkeys != null) {
				for (ForeignKey fk : fkeys) {
					if (fk.getToGroep().isPrimary()) {
						database.addForeignKey(fk);
					} else {
						throw new RdbmsException(
								"foreign key mag alleen naar een primary key wijzen!");
					}
				}
			}
			database.addTabel(tabel);
			return tabel;
		} else {
			throw new RdbmsException("Tabel moet een primary key bevatten!");
		}
	}

	public void fixFKs() {
		for (ForeignKey fk : database.getFkeys()) {
			if (fk.getToGroep().getTabel() == null) {

				fk.getToGroep().setTabel(
						getTabel(fk.getToGroep().getTabelNaam()));
				for(GroepAttr attr : fk.getToGroep().getGroepAttrs()){
					attr.setTabel(fk.getToGroep().getTabel());
				}

			}
		}
	}

	public void dropTable(String naam) throws RdbmsException {
		List<Tabel> tabellen = database.getTabellen();
		Tabel tabel = getTabel(naam);
		if (tabel == null) {
			throw new RdbmsException("Tabel " + naam + " niet gevonden!");
		}
		ArrayList<ForeignKey> toRemoveFK = new ArrayList<ForeignKey>();
		for (ForeignKey fk : database.getFkeys()) {
			if (fk.getToGroep().getTabel() == tabel) {
				throw new RdbmsException(
						"Constraint fout: er wijst nog een foreign key naar de tabel "
								+ naam + "!");
			}
			if (fk.getToGroep().getTabel() == tabel) {
				toRemoveFK.add(fk);
			}
		}
		database.getFkeys().removeAll(toRemoveFK);
		ArrayList<Groep> toRemoveGroep = new ArrayList<Groep>();
		for (Groep groep : database.getGroepen()) {
			if (groep.getTabel() == tabel) {
				toRemoveGroep.add(groep);
			}
		}
		database.getGroepen().removeAll(toRemoveGroep);
		tabellen.remove(tabel);
	}

	public void addAlternativeKey(Tabel tabel, Groep alternative)
			throws RdbmsException {
		if (alternative.isAlternative()) {
			alternative.setTabel(tabel);
			for (GroepAttr attr : alternative.getGroepAttrs()) {
				if (!tabel.getVelden().contains(attr.getVeld())) {
					throw new RdbmsException(
							"Velden in een groep moeten in dezelfde tabel bestaan!");
				}
				attr.setTabel(tabel);
			}
			tabel.addTabelIndex(alternative);
		} else {
			throw new RdbmsException(
					"addAlternativeKey mag alleen groepen met isAlternative==true toevoegen!");
		}
	}

	public void dropAlternativeKey(Tabel tabel, Groep alternative) {
		List<Groep> indices = tabel.getTabelIndex();
		indices.remove(alternative);
	}

	// ============================= DML =================================

	public void insert(String tabelNaam, List<String> velden,
			List<Object> waardes) throws RdbmsException {
		Tabel tabel = getTabel(tabelNaam);
		if (velden.size() != waardes.size()) {
			throw new RdbmsException(
					"Aantal velden en aantal waardes komen niet overeen!");
		}
		ArrayList<Veld> veldLijst = new ArrayList<Veld>();
		if (tabel == null) {
			throw new RdbmsException("Tabel " + tabelNaam + " niet gevonden!");
		}
		for (String veldNaam : velden) {
			Veld veld = getVeld(veldNaam, tabel);
			if (veld == null) {
				throw new RdbmsException("Veld niet gevonden in de tabel!");
			} else {
				veldLijst.add(veld);
			}
		}
		for (Veld veld : veldLijst) {
			String waarde = (String) waardes.get(veldLijst.indexOf(veld));
			// Check if unieke pk waarde
			List<Groep> groepen = database.getGroepen();
			for (Groep groep : groepen) {
				if (groep.isPrimary()) {
					for (GroepAttr attr : groep.getGroepAttrs()) {
						if (attr.getVeld() == veld && attr.getTabel() == tabel) {
							List<Object> values = tabel.getTabelWaarden().get(
									veld);
							if (values != null) {
								for (Object obj : values) {
									String bestaandeWaarde = (String) obj;
									if (bestaandeWaarde.equals(waarde)) {
										throw new RdbmsException(
												"Insertfout: Waarde voor Primary key "
														+ veld.getNaam()
														+ " moet uniek zijn!");
									}
								}
							}
						}
					}
				}
			}

			if (veld.getType().equals("int")) {
				try {
					Integer.parseInt(waarde);
				} catch (NumberFormatException e) {
					throw new RdbmsException(
							"Waarde komt niet overeen met type!");
				}
			} else if (veld.getType().equals("boolean")) {
				if (!waarde.equalsIgnoreCase("true")
						|| !waarde.equalsIgnoreCase("false")) {
					throw new RdbmsException(
							"Waarde komt niet overeen met type!");
				}
			}
		}
		for (ForeignKey fk : database.getFkeys()) {
			if (fk.getFromGroep().getTabel() == tabel) {
				for (GroepAttr attr : fk.getFromGroep().getGroepAttrs()) {
					if (veldLijst.contains(attr.getVeld())) {
						int index = veldLijst.indexOf(attr.getVeld());
						String waarde = (String) waardes.get(index);
						boolean aanwezig = false;
						for (GroepAttr toAttr : fk.getToGroep().getGroepAttrs()) {
							Map<Veld, ArrayList<Object>> tabelWaarden = fk
									.getToGroep().getTabel().getTabelWaarden();
							ArrayList<Object> veldWaarden = tabelWaarden
									.get(toAttr.getVeld());
							if (veldWaarden == null) {
								veldWaarden = new ArrayList<Object>();
							}
							if (veldWaarden.size() == 0) {
								throw new RdbmsException(
										"foreign key verwijst naar niet bestaande waarde!");
							} else {
								for (Object veldWaarde : veldWaarden) {
									String vWaarde = (String) veldWaarde;
									if (vWaarde.equalsIgnoreCase(waarde)) {
										aanwezig = true;
									}
								}
							}
						}
						if (!aanwezig) {
							throw new RdbmsException(
									"foreign key verwijst naar niet bestaande waarde!");
						}
					}
				}
			}
		}

		// Check veld notNull
		for (Veld veld : tabel.getVelden()) {
			if (!veldLijst.contains(veld)) {
				if (veld.isNotNull()) {
					throw new RdbmsException("Veld " + veld.getNaam()
							+ " mag niet null zijn!");
				}
				tabel.addVeldwaarde(veld, "null");
			}
		}
		for (Veld veld : veldLijst) {
			tabel.addVeldwaarde(veld, waardes.get(veldLijst.indexOf(veld)));
		}
	}

	public Map<Veld, ArrayList<Object>> select(String tabelNaam,
			Criteria criteria) throws RdbmsException {
		Tabel tabel = getTabel(tabelNaam);
		if (tabel == null) {
			throw new RdbmsException("Tabel " + tabelNaam + " niet gevonden!");
		}
		Map<Veld, ArrayList<Object>> waardes = tabel.getTabelWaarden();

		// Select * from tabel
		if (criteria == null) {
			return waardes;
		}
		// Select * from tabel WHERE veldnaam </=/>
		else {
			Veld veld = getVeld(criteria.getVeldNaam(), tabel);
			ArrayList<Object> values = waardes.get(veld);
			Map<Veld, ArrayList<Object>> veldMap = new HashMap<Veld, ArrayList<Object>>();
			ArrayList<Integer> veldIndex = new ArrayList<Integer>();
			if (veld.getType().equalsIgnoreCase("int")) {
				for (Object obj : values) {
					int toCheck = Integer.parseInt((String) obj);
					int value = Integer.parseInt(criteria.getValue());
					if (criteria.getWhereCheck().equals("=")) {
						if (toCheck == value) {
							veldIndex.add(values.indexOf(obj));
						}
					} else if (criteria.getWhereCheck().equals(">")) {
						if (toCheck > value) {
							veldIndex.add(values.indexOf(obj));
						}
					} else if (criteria.getWhereCheck().equals("<")) {
						if (toCheck < value) {
							veldIndex.add(values.indexOf(obj));
						}
					}
				}
			}
			if (veld.getType().equalsIgnoreCase("string")) {
				for (Object obj : values) {
					String toCheck = (String) obj;
					String value = criteria.getValue();
					int compare = toCheck.compareToIgnoreCase(value);
					if (criteria.getWhereCheck().equals("=")) {
						if (compare == 0) {
							veldIndex.add(values.indexOf(obj));
						}
					} else if (criteria.getWhereCheck().equals(">")) {
						if (compare > 0) {
							veldIndex.add(values.indexOf(obj));
						}
					} else if (criteria.getWhereCheck().equals("<")) {
						if (compare < 0) {
							veldIndex.add(values.indexOf(obj));
						}
					}
				}
			}
			if (veld.getType().equalsIgnoreCase("boolean")) {
				for (Object obj : values) {
					boolean toCheck = Boolean.parseBoolean((String) obj);
					boolean value = Boolean.parseBoolean(criteria.getValue());
					if (criteria.getWhereCheck().equals("=")) {
						if (toCheck == value) {
							veldIndex.add(values.indexOf(obj));
						}
					} else {
						throw new RdbmsException(
								"Boolean ondersteunt alleen '=' en geen '>' of '<'");
					}
				}
			}
			for (Veld veld1 : waardes.keySet()) {
				ArrayList<Object> valuesVanVeld = waardes.get(veld1);
				ArrayList<Object> toAdd = new ArrayList<Object>();
				for (Integer i : veldIndex) {
					toAdd.add(valuesVanVeld.get(i));
				}
				veldMap.put(veld1, toAdd);
			}
			return veldMap;
		}
	}

	public void delete(String tabelNaam, Criteria criteria)
			throws RdbmsException {
		Tabel tabel = getTabel(tabelNaam);
		if (tabel == null) {
			throw new RdbmsException("Tabel " + tabelNaam + " niet gevonden!");
		}
		if (criteria == null) {
			// DELETE FROM tabel
			for (ForeignKey fk : database.getFkeys()) {
				if (fk.getToGroep().getTabel() == tabel) {
					for (GroepAttr attr : fk.getFromGroep().getGroepAttrs()) {
						Tabel from = fk.getFromGroep().getTabel();
						ArrayList<Object> waardes = from.getTabelWaarden().get(
								attr.getVeld());
						if (waardes != null && waardes.size() > 0) {
							throw new RdbmsException(
									"Kan tabel: "
											+ tabel
											+ " niet leegmaken, er wijst nog een foreign key naartoe!");

						}
					}
				}
			}
			tabel.setTabelWaarden(new HashMap<Veld, ArrayList<Object>>());
		} else {
			// DELETE from tabel WHERE veldnaam </=/>
			Map<Veld, ArrayList<Object>> waardes = tabel.getTabelWaarden();
			Veld veld = getVeld(criteria.getVeldNaam(), tabel);
			ArrayList<Object> values = waardes.get(veld);
			ArrayList<Integer> toRemove = new ArrayList<Integer>();
			if (values != null) {

				if (veld.getType().equalsIgnoreCase("int")) {
					for (Object obj : values) {
						int toCheck = Integer.parseInt((String) obj);
						int value = Integer.parseInt(criteria.getValue());
						if (criteria.getWhereCheck().equals("=")) {
							if (toCheck == value) {
								toRemove.add(values.indexOf(obj));
							}
						} else if (criteria.getWhereCheck().equals(">")) {
							if (toCheck > value) {
								toRemove.add(values.indexOf(obj));
							}
						} else if (criteria.getWhereCheck().equals("<")) {
							if (toCheck < value) {
								toRemove.add(values.indexOf(obj));
							}
						}
					}
				}
				if (veld.getType().equalsIgnoreCase("string")) {
					for (Object obj : values) {
						String toCheck = (String) obj;
						String value = criteria.getValue();
						int compare = toCheck.compareToIgnoreCase(value);
						if (criteria.getWhereCheck().equals("=")) {
							if (compare == 0) {
								toRemove.add(values.indexOf(obj));
							}
						} else if (criteria.getWhereCheck().equals(">")) {
							if (compare > 0) {
								toRemove.add(values.indexOf(obj));
							}
						} else if (criteria.getWhereCheck().equals("<")) {
							if (compare < 0) {
								toRemove.add(values.indexOf(obj));
							}
						}
					}
				}
				if (veld.getType().equalsIgnoreCase("boolean")) {
					for (Object obj : values) {
						boolean toCheck = (boolean) obj;
						boolean value = Boolean.parseBoolean(criteria
								.getValue());
						if (criteria.getWhereCheck().equals("=")) {
							if (toCheck == value) {
								toRemove.add(values.indexOf(obj));
							}
						} else {
							throw new RdbmsException(
									"Boolean ondersteunt alleen '=' en geen '>' of '<'");
						}
					}
				}
				for (Veld veld1 : waardes.keySet()) {
					ArrayList<Object> valuesVanVeld = waardes.get(veld1);
					for (Integer i : toRemove) {
						String waarde = (String) valuesVanVeld.get(i);
						for (ForeignKey fk : database.getFkeys()) {
							if (fk.getToGroep().getTabel() == tabel) {
								for (GroepAttr attr : fk.getFromGroep()
										.getGroepAttrs()) {
									ArrayList<Object> valuesFrom = fk
											.getFromGroep().getTabel()
											.getTabelWaarden()
											.get(attr.getVeld());
									for (Object obj : valuesFrom) {
										if (waarde.equals((String) obj)) {
											throw new RdbmsException(
													"Verwijderen niet toegestaan, er wijst een foreign key naar het veld "
															+ veld1.getNaam()
															+ "!");
										}
									}
								}
							}
						}
					}
				}
				for (Veld veld1 : waardes.keySet()) {
					ArrayList<Object> valuesVanVeld = waardes.get(veld1);
					for (Integer i : toRemove) {
						valuesVanVeld.remove(i.intValue());
					}
					waardes.put(veld1, valuesVanVeld);
				}
			}

		}
	}

	public void printDb() {
		for (Database db : databases) {
			for (Groep g : db.getGroepen()) {
				System.out.println(g.toString());
			}
		}
	}

	public void saveDatabases() {
		try {
			DBManager dbm = new DBManager();
			Connection con = dbm.openConnection();
			for (Database db : gedropteDatabases) {
				for (ForeignKey fKey : db.getFkeys()) {
					con.prepareStatement(
							"DELETE FROM foreign_key WHERE from_groep='"
									+ fKey.getFromGroep().getId() + "'")
							.execute();
				}
				con.prepareStatement(
						"DELETE FROM groep_attr WHERE database_naam='"
								+ db.getNaam() + "'").execute();
				con.prepareStatement(
						"DELETE FROM groep WHERE database_naam='"
								+ db.getNaam() + "'").execute();
				con.prepareStatement(
						"DELETE FROM veld WHERE database_naam='" + db.getNaam()
								+ "'").execute();
				con.prepareStatement(
						"DELETE FROM tabel WHERE database_naam='"
								+ db.getNaam() + "'").execute();
				con.prepareStatement(
						"DELETE FROM database_impl WHERE naam='" + db.getNaam()
								+ "'").execute();
			}
			for (Database db : nieuweDatabases) {
				PreparedStatement p1 = con
						.prepareStatement("INSERT INTO database_impl VALUES(?)");
				p1.setString(1, db.getNaam());
				p1.execute();
				for (Tabel tabel : db.getTabellen()) {
					PreparedStatement p2 = con
							.prepareStatement("INSERT INTO tabel VALUES(?, ?)");
					p2.setString(1, tabel.getNaam());
					p2.setString(2, db.getNaam());
					p2.execute();
					for (Veld veld : tabel.getVelden()) {
						PreparedStatement p3 = con
								.prepareStatement("INSERT INTO veld VALUES(?, ?, ?, ?, ?)");
						p3.setString(1, veld.getNaam());
						p3.setString(2, tabel.getNaam());
						p3.setString(3, db.getNaam());
						p3.setString(4, veld.getType());
						p3.setBoolean(5, veld.isNotNull());
						p3.execute();
					}
					for (Groep groep : tabel.getTabelIndex()) {
						PreparedStatement p4 = con
								.prepareStatement("INSERT INTO groep (tabel_naam, database_naam, isPrimary, isAlternative) VALUES(?, ?, ?, ?)");
						p4.setString(1, groep.getTabel().getNaam());
						p4.setString(2, groep.getDatabase().getNaam());
						p4.setBoolean(3, groep.isPrimary());
						p4.setBoolean(4, groep.isAlternative());
						p4.execute();

						int groepId = getIdFromGroep(con, groep);

						for (GroepAttr attr : groep.getGroepAttrs()) {
							PreparedStatement p5 = con
									.prepareStatement("INSERT INTO groep_attr VALUES(?, ?, ?, ?)");
							p5.setInt(1, groepId);
							p5.setString(2, attr.getVeld().getNaam());
							p5.setString(3, attr.getTabel().getNaam());
							p5.setString(4, attr.getDatabase().getNaam());
							p5.execute();
						}
					}
				}
				for (Groep groep : db.getGroepen()) {
					PreparedStatement p4 = con
							.prepareStatement("INSERT INTO groep (tabel_naam, database_naam, isPrimary, isAlternative) VALUES(?, ?, ?, ?)");
					p4.setString(1, groep.getTabel().getNaam());
					p4.setString(2, groep.getDatabase().getNaam());
					p4.setBoolean(3, groep.isPrimary());
					p4.setBoolean(4, groep.isAlternative());
					p4.execute();

					// TODO FIX id
					int groepId = getIdFromGroep(con, groep);
					groep.setId(groepId);
					for (GroepAttr attr : groep.getGroepAttrs()) {
						PreparedStatement p5 = con
								.prepareStatement("INSERT INTO groep_attr VALUES(?, ?, ?, ?)");
						p5.setInt(1, groepId);
						p5.setString(2, attr.getVeld().getNaam());
						p5.setString(3, attr.getTabel().getNaam());
						p5.setString(4, attr.getDatabase().getNaam());
						p5.execute();
					}
				}
				for (ForeignKey fk : db.getFkeys()) {
					PreparedStatement p6 = con
							.prepareStatement("INSERT INTO foreign_key VALUES(?, ?)");
					p6.setInt(1, fk.getFromGroep().getId());
					p6.setInt(2, fk.getToGroep().getId());
					p6.execute();
				}
			}
			dbm.closeConnection();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private int getIdFromGroep(Connection con, Groep groep) throws SQLException {
		PreparedStatement ps = con
				.prepareStatement("SELECT id FROM groep WHERE tabel_naam = ? AND database_naam = ? AND isPrimary = ? AND isAlternative = ?");
		ps.setString(1, groep.getTabel().getNaam());
		ps.setString(2, groep.getDatabase().getNaam());
		ps.setBoolean(3, groep.isPrimary());
		ps.setBoolean(4, groep.isAlternative());
		ResultSet rs = ps.executeQuery();
		int groepId = 0;
		while (rs.next()) {
			groepId = rs.getInt(1);
		}
		return groepId;
	}

	private Database getDatabase(String naam) {
		for (Database db : databases) {
			if (db.getNaam().equals(naam)) {
				return db;
			}
		}
		return null;
	}

	private Database removeDatabase(String naam) {
		Database datab = null;
		for (Database db : databases) {
			if (db.getNaam().equals(naam)) {
				datab = db;
			}
		}
		if (datab != null) {
			if (databases.remove(datab)) {
				return datab;
			}
		}
		return null;
	}

	private Tabel getTabel(String naam) {
		for (Tabel t : database.getTabellen()) {
			if (t.getNaam().equals(naam)) {
				return t;
			}
		}
		return null;
	}

	private Veld getVeld(String naam, Tabel tabel) {
		for (Veld veld : tabel.getVelden()) {
			if (veld.getNaam().equals(naam)) {
				return veld;
			}
		}
		return null;
	}

	//
	// public Groep getGroep(int id, String databaseNaam) {
	// Database db = getDatabase(databaseNaam);
	// assert (db != null);
	//
	// for (Groep g : db.getGroepen()) {
	// if (g.getId() == id) {
	// return g;
	// }
	// }
	// return null;
	// }

	public List<Database> getDatabases() {
		return databases;
	}

	public void setDatabases(List<Database> databases) {
		this.databases = databases;
	}

	public void setNieuweDatabases(List<Database> databases) {
		nieuweDatabases = databases;
	}

	public Database getDatabase() {
		return database;
	}

}
